/*-------------------------------------------------------------------------
 *
 * genam.c
 *	  一般索引访问方法例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/index/genam.c
 *
 * NOTES
 *	  许多旧的访问方法例程已被转换为
 *	  宏并移动到 genam.h -cim 4/30/91
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/genam.h"
#include "access/heapam.h"
#include "access/relscan.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "catalog/catalog.h"
#include "catalog/index.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
#include "storage/procarray.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/rls.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"


/* ----------------------------------------------------------------
 *		一般访问方法例程
 *
 *		所有索引访问方法使用相同的扫描结构。
 *		我们不知道各种AM是如何进行锁定的，因此在这里
 *		不对此进行处理。
 *
 *		目的是AM实现者将定义一个beginscan例程，
 *		调用RelationGetIndexScan以填充扫描，然后进行
 *		他想要的任何类型的锁定。
 *
 *		在扫描结束时，AM的endscan例程撤销锁定，
 *		但*不*调用IndexScanEnd --- 更高层次的index_endscan
 *		例程会执行此操作。 （我们不能在AM中执行此操作，
 *		因为index_endscan在调用AM之后仍然需要接触IndexScanDesc。）
 *
 *		因此，AM是否调用RelationGetIndexScan是没有选择的；
 *		它的beginscan例程必须返回由RelationGetIndexScan生成的对象。
 *		这有点丑，但现在不值得清理。
 * ----------------------------------------------------------------
 */

/* ----------------
 *	RelationGetIndexScan -- 创建并填充一个IndexScanDesc。
 *
 *		该例程创建一个索引扫描结构，并为其设置初始
 *		内容。
 *
 *		参数：
 *				indexRelation -- 用于扫描的索引关系。
 *				nkeys -- 扫描键的数量（索引资格条件）。
 *				norderbys -- 索引排序操作符的数量。
 *
 *		返回：
 *				一个初始化的IndexScanDesc。
 * ----------------
 */
IndexScanDesc RelationGetIndexScan(Relation fc_indexRelation, int fc_nkeys, int fc_norderbys)
{
	IndexScanDesc fc_scan;

	fc_scan = (IndexScanDesc) palloc(sizeof(IndexScanDescData));

	fc_scan->heapRelation = NULL;	/* 可以稍后设置 */
	fc_scan->xs_heapfetch = NULL;
	fc_scan->indexRelation = fc_indexRelation;
	fc_scan->xs_snapshot = InvalidSnapshot;	/* 调用者必须初始化此项 */
	fc_scan->numberOfKeys = fc_nkeys;
	fc_scan->numberOfOrderBys = fc_norderbys;

	/*
	 * 我们在这里分配键工作空间，但它不会在amrescan之前被填充。
	 */
	if (fc_nkeys > 0)
		fc_scan->keyData = (ScanKey) palloc(sizeof(ScanKeyData) * fc_nkeys);
	else
		fc_scan->keyData = NULL;
	if (fc_norderbys > 0)
		fc_scan->orderByData = (ScanKey) palloc(sizeof(ScanKeyData) * fc_norderbys);
	else
		fc_scan->orderByData = NULL;

	fc_scan->xs_want_itup = false; /* 可以稍后设置 */

	/*
	 * 在恢复期间，我们忽略已删除的元组，也不去处理它们。
	 * 我们这样做是因为主节点上的xmin很可能晚于
	 * 备用节点上的xmin，因此主节点认为已删除的元组
	 * 可能在备用节点上是可见的。因此，为了在恢复期间正确
	 * 实现MVCC查询，我们必须忽略这些提示并检查所有元组。
	 * 在恢复期间启动的事务中*不要*将ignore_killed_tuples设置为true。
	 * xactStartedInRecovery不应被索引AM修改。
	 */
	fc_scan->kill_prior_tuple = false;
	fc_scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
	fc_scan->ignore_killed_tuples = !fc_scan->xactStartedInRecovery;

	fc_scan->opaque = NULL;

	fc_scan->xs_itup = NULL;
	fc_scan->xs_itupdesc = NULL;
	fc_scan->xs_hitup = NULL;
	fc_scan->xs_hitupdesc = NULL;

	return fc_scan;
}

/* ----------------
 *	IndexScanEnd -- 结束索引扫描。
 *
 *		该例程仅释放由RelationGetIndexScan()获取的存储。
 *		任何AM级资源都假定已由AM的endscan例程释放。
 *
 *	返回：
 *		无。
 * ----------------
 */
void IndexScanEnd(IndexScanDesc fc_scan)
{
	if (fc_scan->keyData != NULL)
		pfree(fc_scan->keyData);
	if (fc_scan->orderByData != NULL)
		pfree(fc_scan->orderByData);

	pfree(fc_scan);
}

/*
 * BuildIndexValueDescription
 *
 * 构造一个描述索引条目内容的字符串，形式为 "(key_name, ... )=(key_value, ... )"。
 * 这目前用于构建唯一约束和排除约束错误消息，因此仅检查和打印索引的键列。
 *
 * 注意，如果用户没有权限查看所有相关列，则返回 NULL。
 * 返回部分键似乎不太实用，我们无法知道用户提供了哪些列（与 ExecBuildSlotValueDescription 不同）。
 *
 * 传入的值/NULL 数组是索引 AM 的“原始”输入，例如 FormIndexDatum 的结果——这不一定是存储在索引中的内容，但这是用户认为存储的内容。
 *
 * 注意：如果更改这里的任何内容，请检查 execMain.c 中的 ExecBuildSlotPartitionKeyDescription() 是否需要类似的更改。
 */
char * BuildIndexValueDescription(Relation fc_indexRelation,
						   Datum *fc_values, bool *fc_isnull)
{
	StringInfoData fc_buf;
	Form_pg_index fc_idxrec;
	int			fc_indnkeyatts;
	int			fc_i;
	int			fc_keyno;
	Oid			fc_indexrelid = RelationGetRelid(fc_indexRelation);
	Oid			fc_indrelid;
	AclResult	fc_aclresult;

	fc_indnkeyatts = IndexRelationGetNumberOfKeyAttributes(fc_indexRelation);

	/*
	 * 检查权限 - 如果用户没有访问所有键列的权限，则返回 NULL 以避免泄漏数据。
	 *
	 * 首先检查关系是否启用 RLS。如果是，返回 NULL 以避免泄漏数据。
	 *
	 * 然后我们需要检查表级 SELECT 访问，如果没有访问，再检查列级权限。
	 */
	fc_idxrec = fc_indexRelation->rd_index;
	fc_indrelid = fc_idxrec->indrelid;
	Assert(fc_indexrelid == fc_idxrec->indexrelid);

	/* RLS 检查 - 如果启用了 RLS，则我们不返回任何内容。 */
	if (check_enable_rls(fc_indrelid, InvalidOid, true) == RLS_ENABLED)
		return NULL;

	/* 表级 SELECT 足够，如果用户有此权限 */
	fc_aclresult = pg_class_aclcheck(fc_indrelid, GetUserId(), ACL_SELECT);
	if (fc_aclresult != ACLCHECK_OK)
	{
		/*
		 * 没有表级访问，因此遍历索引中的列，确保用户在所有这些列上具有 SELECT 权限。
		 */
		for (fc_keyno = 0; fc_keyno < fc_indnkeyatts; fc_keyno++)
		{
			AttrNumber	fc_attnum = fc_idxrec->indkey.values[fc_keyno];

			/*
			 * 注意，如果 attnum == InvalidAttrNumber，则这是基于表达式的索引，
			 * 我们不返回任何细节，而不是尝试弄清楚表达式包含哪些列以及用户是否在这些列上有 SELECT 权限。
			 */
			if (fc_attnum == InvalidAttrNumber ||
				pg_attribute_aclcheck(fc_indrelid, fc_attnum, GetUserId(),
									  ACL_SELECT) != ACLCHECK_OK)
			{
				/* 没有访问权限，因此清理并返回 */
				return NULL;
			}
		}
	}

	initStringInfo(&fc_buf);
	appendStringInfo(&fc_buf, "(%s)=(",
					 pg_get_indexdef_columns(fc_indexrelid, true));

	for (fc_i = 0; fc_i < fc_indnkeyatts; fc_i++)
	{
		char	   *fc_val;

		if (fc_isnull[fc_i])
			fc_val = "null";
		else
		{
			Oid			fc_foutoid;
			bool		fc_typisvarlena;

			/*
			 * 提供的数据不一定是存储在索引中的类型；而是索引 opclass 的输入类型。因此查看 rd_opcintype，而不是索引 tupdesc。
			 *
			 * 注意：对于具有伪类型输入类型的 opclass，例如 ANYARRAY 或 RECORD，这一点有些不稳定。
			 * 目前，与这些伪类型关联的 typoutput 函数将正常工作，但将来我们可能需要更加努力。
			 */
			getTypeOutputInfo(fc_indexRelation->rd_opcintype[fc_i],
							  &fc_foutoid, &fc_typisvarlena);
			fc_val = OidOutputFunctionCall(fc_foutoid, fc_values[fc_i]);
		}

		if (fc_i > 0)
			appendStringInfoString(&fc_buf, ", ");
		appendStringInfoString(&fc_buf, fc_val);
	}

	appendStringInfoChar(&fc_buf, ')');

	return fc_buf.data;
}

/*
 * 从被删除的索引元组指向的表条目中获取最新的 removedXid
 * 使用 AM 泛化的方法。
 *
 * 这是一个 table_index_delete_tuples() 的 shim，用于具有
 * 简单要求的索引 AM。这些调用者只需要咨询 tableam 以获取
 * latestRemovedXid 值，并只期望删除已知可删除的元组。
 * 当在索引 AM 的删除 WAL 记录中不需要 latestRemovedXid 值时，
 * 完全跳过此调用是安全的。
 *
 * 我们假设调用者索引 AM 使用标准的 IndexTuple 表示，
 * 表 TIDs 存储在 t_tid 字段中。我们还期望（并断言）
 * 页面上的行指针对于 'itemnos' 偏移量已经标记为
 * LP_DEAD。
 */
TransactionId index_compute_xid_horizon_for_tuples(Relation fc_irel,
									 Relation fc_hrel,
									 Buffer fc_ibuf,
									 OffsetNumber *fc_itemnos,
									 int fc_nitems)
{
	TM_IndexDeleteOp fc_delstate;
	TransactionId fc_latestRemovedXid = InvalidTransactionId;
	Page		fc_ipage = BufferGetPage(fc_ibuf);
	IndexTuple	fc_itup;

	Assert(fc_nitems > 0);

	fc_delstate.irel = fc_irel;
	fc_delstate.iblknum = BufferGetBlockNumber(fc_ibuf);
	fc_delstate.bottomup = false;
	fc_delstate.bottomupfreespace = 0;
	fc_delstate.ndeltids = 0;
	fc_delstate.deltids = palloc(fc_nitems * sizeof(TM_IndexDelete));
	fc_delstate.status = palloc(fc_nitems * sizeof(TM_IndexStatus));

	/* 确定即将被删除的索引元组指向的内容 */
	for (int fc_i = 0; fc_i < fc_nitems; fc_i++)
	{
		OffsetNumber fc_offnum = fc_itemnos[fc_i];
		ItemId		fc_iitemid;

		fc_iitemid = PageGetItemId(fc_ipage, fc_offnum);
		fc_itup = (IndexTuple) PageGetItem(fc_ipage, fc_iitemid);

		Assert(ItemIdIsDead(fc_iitemid));

		ItemPointerCopy(&fc_itup->t_tid, &fc_delstate.deltids[fc_i].tid);
		fc_delstate.deltids[fc_i].id = fc_delstate.ndeltids;
		fc_delstate.status[fc_i].idxoffnum = fc_offnum;
		fc_delstate.status[fc_i].knowndeletable = true;	/* 标记为 LP_DEAD */
		fc_delstate.status[fc_i].promising = false;	/* 未使用 */
		fc_delstate.status[fc_i].freespace = 0;	/* 未使用 */

		fc_delstate.ndeltids++;
	}

	/* 确定实际的 xid 地平线 */
	fc_latestRemovedXid = table_index_delete_tuples(fc_hrel, &fc_delstate);

	/* 断言 tableam 同意所有条目可删除 */
	Assert(fc_delstate.ndeltids == fc_nitems);

	pfree(fc_delstate.deltids);
	pfree(fc_delstate.status);

	return fc_latestRemovedXid;
}


/* ----------------------------------------------------------------
 *		堆或索引扫描访问系统目录
 *
 *		这些函数支持通常使用索引的系统目录访问，
 *		但需要能够在系统索引不可用时切换到堆扫描。
 *
 *		指定的扫描键必须与命名的索引兼容。
 *		通常这意味着它们必须约束索引的所有列，
 *		或者一个 N 列索引的前 K 列。
 *
 *		实际上，这些例程可以与非系统表一起使用，
 *		但只有在有已知索引可以与给定扫描键一起使用时才有用；
 *		因此在实践中它们仅适用于系统目录的预定类型的扫描。
 * ----------------------------------------------------------------
 */

/*
 * systable_beginscan --- 为堆或索引扫描做好准备
 *
 *	rel: 要扫描的目录，已经打开并适当锁定
 *	indexId: 条件使用的索引的 OID
 *	indexOK: 如果为 false，则强制进行堆扫描（见下文的说明）
 *	snapshot: 要使用的时间条件（NULL 表示最近的目录快照）
 *	nkeys, key: 扫描键
 *
 * 扫描键中的属性编号应为堆情况设置。
 * 如果选择索引，则将其重置为 1..n 以引用索引
 * 列。请注意，这意味着每个索引列必须有一个 scankey 资格！
 * 这在正常的使用索引的情况下通过断言检查，但如果采用堆扫描路径则不会检查。
 *
 * 例程检查索引扫描是否安全的正常情况，
 * 但调用者可以进行额外检查，并在需要时通过 indexOK=false。
 * 在标准情况下，indexOK 可以简单地是常量 TRUE。
 */
SysScanDesc
systable_beginscan(Relation fc_heapRelation,
				   Oid fc_indexId,
				   bool fc_indexOK,
				   Snapshot fc_snapshot,
				   int fc_nkeys, ScanKey fc_key)
{
	SysScanDesc fc_sysscan;
	Relation	fc_irel;

	if (fc_indexOK &&
		!IgnoreSystemIndexes &&
		!ReindexIsProcessingIndex(fc_indexId))
		fc_irel = index_open(fc_indexId, AccessShareLock);
	else
		fc_irel = NULL;

	fc_sysscan = (SysScanDesc) palloc(sizeof(SysScanDescData));

	fc_sysscan->heap_rel = fc_heapRelation;
	fc_sysscan->irel = fc_irel;
	fc_sysscan->slot = table_slot_create(fc_heapRelation, NULL);

	if (fc_snapshot == NULL)
	{
		Oid			fc_relid = RelationGetRelid(fc_heapRelation);

		fc_snapshot = RegisterSnapshot(GetCatalogSnapshot(fc_relid));
		fc_sysscan->snapshot = fc_snapshot;
	}
	else
	{
		/* 调用者负责任何快照。 */
		fc_sysscan->snapshot = NULL;
	}

	if (fc_irel)
	{
		int			fc_i;

		/* 将属性编号更改为索引列编号。 */
		for (fc_i = 0; fc_i < fc_nkeys; fc_i++)
		{
			int			fc_j;

			for (fc_j = 0; fc_j < IndexRelationGetNumberOfAttributes(fc_irel); fc_j++)
			{
				if (fc_key[fc_i].sk_attno == fc_irel->rd_index->indkey.values[fc_j])
				{
					fc_key[fc_i].sk_attno = fc_j + 1;
					break;
				}
			}
			if (fc_j == IndexRelationGetNumberOfAttributes(fc_irel))
				elog(ERROR, "column is not in index");
		}

		fc_sysscan->iscan = index_beginscan(fc_heapRelation, fc_irel,
										 fc_snapshot, fc_nkeys, 0);
		index_rescan(fc_sysscan->iscan, fc_key, fc_nkeys, NULL, 0);
		fc_sysscan->scan = NULL;
	}
	else
	{
		/*
		 * 我们不允许在强制使用堆扫描检索目录时进行同步扫描。在大多数情况下，所需的行在前面，因此同步扫描的不确定起点是一个严重的缺点；并且没有补偿性优势，因为这种扫描不太可能并行发生。
		 */
		fc_sysscan->scan = table_beginscan_strat(fc_heapRelation, fc_snapshot,
											  fc_nkeys, fc_key,
											  true, false);
		fc_sysscan->iscan = NULL;
	}

	/*
	 * 如果设置了CheckXidAlive，则设置一个标志以指示系统表扫描正在进行中。请参见xact.c中声明这些变量的详细注释。
	 */
	if (TransactionIdIsValid(CheckXidAlive))
		bsysscan = true;

	return fc_sysscan;
}

/*
 * HandleConcurrentAbort - 处理CheckXidAlive的并发中止。
 *
 * 如果CheckXidAlive被中止，则报错。我们不能直接使用TransactionIdDidAbort，因为在崩溃后，该事务可能没有被标记为已中止。请参见xact.c中声明变量的详细注释。
 */
static inline void fc_HandleConcurrentAbort()
{
	if (TransactionIdIsValid(CheckXidAlive) &&
		!TransactionIdIsInProgress(CheckXidAlive) &&
		!TransactionIdDidCommit(CheckXidAlive))
		ereport(ERROR,
				(errcode(ERRCODE_TRANSACTION_ROLLBACK),
				 errmsg("transaction aborted during system catalog scan")));
}

/*
 * systable_getnext --- 在堆或索引扫描中获取下一个元组
 *
 * 如果没有更多元组可用，则返回NULL。
 *
 * 请注意，返回的元组是对磁盘缓冲区中数据的引用；它必须不能被修改，并且在下一次getnext()或endscan()调用后应被认为不可访问。
 *
 * XXX: 提供基于槽的接口，至少作为选择，可能是有意义的。
 */
HeapTuple systable_getnext(SysScanDesc fc_sysscan)
{
	HeapTuple	fc_htup = NULL;

	if (fc_sysscan->irel)
	{
		if (index_getnext_slot(fc_sysscan->iscan, ForwardScanDirection, fc_sysscan->slot))
		{
			bool		fc_shouldFree;

			fc_htup = ExecFetchSlotHeapTuple(fc_sysscan->slot, false, &fc_shouldFree);
			Assert(!fc_shouldFree);

			/*
			 * 目前我们不需要为任何系统目录扫描支持有损索引操作符。如果我们安排在systable_beginscan()期间保存堆的attnums，使用扫描键来驱动操作符调用，这可以在这里完成；这是可行的，因为我们仍然不需要支持表达式上的索引。
			 */
			if (fc_sysscan->iscan->xs_recheck)
				elog(ERROR, "system catalog scans with lossy index conditions are not implemented");
		}
	}
	else
	{
		if (table_scan_getnextslot(fc_sysscan->scan, ForwardScanDirection, fc_sysscan->slot))
		{
			bool		fc_shouldFree;

			fc_htup = ExecFetchSlotHeapTuple(fc_sysscan->slot, false, &fc_shouldFree);
			Assert(!fc_shouldFree);
		}
	}

	/*
	 * 在对事务进行逻辑流式传输期间，处理在检索目录元组时的并发中止。
	 */
	fc_HandleConcurrentAbort();

	return fc_htup;
}

/*
 * systable_recheck_tuple --- 重新检查最近获取的元组的可见性
 *
 * 特别是，确定该元组在现在开始的目录扫描中是否可见。我们不处理非MVCC扫描快照的情况，因为目前没有调用者需要那样。
 *
 * 这对于测试在我们等待获取锁时对象是否被删除是有用的。
 *
 * 请注意：我们实际上并不*需要*将元组传入，但这是一个很好的交叉检查，表明调用者对正确的元组感兴趣。
 */
bool systable_recheck_tuple(SysScanDesc fc_sysscan, HeapTuple fc_tup)
{
	Snapshot	fc_freshsnap;
	bool		fc_result;

	Assert(fc_tup == ExecFetchSlotHeapTuple(fc_sysscan->slot, false, NULL));

	/*
	 * 信任table_tuple_satisfies_snapshot()及其子函数（通常是LockBuffer()和HeapTupleSatisfiesMVCC()）不会自行获取快照，因此我们不需要注册快照。这些功能过于底层，不适合扫描表。
	 */
	fc_freshsnap = GetCatalogSnapshot(RelationGetRelid(fc_sysscan->heap_rel));

	fc_result = table_tuple_satisfies_snapshot(fc_sysscan->heap_rel,
											fc_sysscan->slot,
											fc_freshsnap);

	/*
	 * 在对事务进行逻辑流式传输期间，处理在检索目录元组时的并发中止。
	 */
	fc_HandleConcurrentAbort();

	return fc_result;
}

/*
 * systable_endscan --- 关闭扫描，释放资源
 *
 * 请注意，关闭堆关系仍由调用者决定。
 */
void systable_endscan(SysScanDesc fc_sysscan)
{
	if (fc_sysscan->slot)
	{
		ExecDropSingleTupleTableSlot(fc_sysscan->slot);
		fc_sysscan->slot = NULL;
	}

	if (fc_sysscan->irel)
	{
		index_endscan(fc_sysscan->iscan);
		index_close(fc_sysscan->irel, AccessShareLock);
	}
	else
		table_endscan(fc_sysscan->scan);

	if (fc_sysscan->snapshot)
		UnregisterSnapshot(fc_sysscan->snapshot);

	/*
	 * 在systable扫描结束时重置bsysscan标志。请参见xact.c中声明这些变量的详细注释。
	 */
	if (TransactionIdIsValid(CheckXidAlive))
		bsysscan = false;

	pfree(fc_sysscan);
}


/*
 * systable_beginscan_ordered --- 设置有序目录扫描
 *
 * 这些例程的API基本上与systable_beginscan等相同，
 * 只是它们保证以索引顺序返回多个匹配的元组。此处，
 * 由于历史原因，需要使用的索引由调用者打开并锁定。
 *
 * 目前我们不支持基于非索引的扫描。(原则上，
 * 我们可以进行堆扫描和排序，但在可能不需要
 * 继续与损坏的目录索引工作的位置使用。)
 * 因此，目前这些函数只是对index_beginscan/index_getnext_slot的
 * 最薄封装。它们存在的主要原因是集中可能的
 * 未来对目录扫描中有损操作符的支持。
 */
SysScanDesc
systable_beginscan_ordered(Relation fc_heapRelation,
						   Relation fc_indexRelation,
						   Snapshot fc_snapshot,
						   int fc_nkeys, ScanKey fc_key)
{
	SysScanDesc fc_sysscan;
	int			fc_i;

	/* REINDEX在此处可能会是一个严重错误... */
	if (ReindexIsProcessingIndex(RelationGetRelid(fc_indexRelation)))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot access index \"%s\" while it is being reindexed",
						RelationGetRelationName(fc_indexRelation))));
	/* ... 但我们只是对违反IgnoreSystemIndexes发出警告 */
	if (IgnoreSystemIndexes)
		elog(WARNING, "using index \"%s\" despite IgnoreSystemIndexes",
			 RelationGetRelationName(fc_indexRelation));

	fc_sysscan = (SysScanDesc) palloc(sizeof(SysScanDescData));

	fc_sysscan->heap_rel = fc_heapRelation;
	fc_sysscan->irel = fc_indexRelation;
	fc_sysscan->slot = table_slot_create(fc_heapRelation, NULL);

	if (fc_snapshot == NULL)
	{
		Oid			fc_relid = RelationGetRelid(fc_heapRelation);

		fc_snapshot = RegisterSnapshot(GetCatalogSnapshot(fc_relid));
		fc_sysscan->snapshot = fc_snapshot;
	}
	else
	{
		/* 调用者负责任何快照。 */
		fc_sysscan->snapshot = NULL;
	}

	/* 将属性编号更改为索引列编号。 */
	for (fc_i = 0; fc_i < fc_nkeys; fc_i++)
	{
		int			fc_j;

		for (fc_j = 0; fc_j < IndexRelationGetNumberOfAttributes(fc_indexRelation); fc_j++)
		{
			if (fc_key[fc_i].sk_attno == fc_indexRelation->rd_index->indkey.values[fc_j])
			{
				fc_key[fc_i].sk_attno = fc_j + 1;
				break;
			}
		}
		if (fc_j == IndexRelationGetNumberOfAttributes(fc_indexRelation))
			elog(ERROR, "column is not in index");
	}

	fc_sysscan->iscan = index_beginscan(fc_heapRelation, fc_indexRelation,
									 fc_snapshot, fc_nkeys, 0);
	index_rescan(fc_sysscan->iscan, fc_key, fc_nkeys, NULL, 0);
	fc_sysscan->scan = NULL;

	/*
	 * 如果设置了CheckXidAlive，则设置一个标志以指示系统表扫描正在进行中。请参见xact.c中声明这些变量的详细注释。
	 */
	if (TransactionIdIsValid(CheckXidAlive))
		bsysscan = true;

	return fc_sysscan;
}

/*
 * systable_getnext_ordered --- 在有序目录扫描中获取下一个元组
 */
HeapTuple systable_getnext_ordered(SysScanDesc fc_sysscan, ScanDirection fc_direction)
{
	HeapTuple	fc_htup = NULL;

	Assert(fc_sysscan->irel);
	if (index_getnext_slot(fc_sysscan->iscan, fc_direction, fc_sysscan->slot))
		fc_htup = ExecFetchSlotHeapTuple(fc_sysscan->slot, false, NULL);

	/* 请参阅systable_getnext中的说明 */
	if (fc_htup && fc_sysscan->iscan->xs_recheck)
		elog(ERROR, "system catalog scans with lossy index conditions are not implemented");

	/*
	 * 在对事务进行逻辑流式传输期间，处理在检索目录元组时的并发中止。
	 */
	fc_HandleConcurrentAbort();

	return fc_htup;
}

/*
 * systable_endscan_ordered --- 关闭扫描，释放资源
 */
void systable_endscan_ordered(SysScanDesc fc_sysscan)
{
	if (fc_sysscan->slot)
	{
		ExecDropSingleTupleTableSlot(fc_sysscan->slot);
		fc_sysscan->slot = NULL;
	}

	Assert(fc_sysscan->irel);
	index_endscan(fc_sysscan->iscan);
	if (fc_sysscan->snapshot)
		UnregisterSnapshot(fc_sysscan->snapshot);

	/*
	 * 在systable扫描结束时重置bsysscan标志。请参见xact.c中声明这些变量的详细注释。
	 */
	if (TransactionIdIsValid(CheckXidAlive))
		bsysscan = false;

	pfree(fc_sysscan);
}

/*
 * systable_inplace_update_begin --- "就地" 更新一行（覆盖它）
 *
 * 覆盖违反了MVCC和事务安全性，因此此函数在Postgres中的
 * 使用非常有限。尽管如此，我们仍然在某些地方找到
 * 使用它的机会。请参见README.tuplock部分 "锁定以写入
 * 就地更新的表 "以及后面的部分，以了解对
 * 进行就地更新的表的读取者和写入者的期望。标准流程：
 *
 * ... [任何不需要oldtup的慢准备] ...
 * systable_inplace_update_begin([...], &tup, &inplace_state);
 * if (!HeapTupleIsValid(tup))
 *	elog(ERROR, [...]);
 * ... [缓冲区被排他锁定; 修改 "tup" ] ...
 * if (dirty)
 *	systable_inplace_update_finish(inplace_state, tup);
 * else
 *	systable_inplace_update_cancel(inplace_state);
 *
 * 前几个参数与systable_beginscan()参数列表重复。
 * "oldtupcopy"是一个输出参数，如果键不再找到活动元组则分配为NULL。
 * （在PROC_IN_VACUUM中，这是一个低概率的瞬态
 * 条件。）如果"oldtupcopy"变为非NULL，您必须将输出参数
 * "state"传递给systable_inplace_update_finish()或
 * systable_inplace_update_cancel()。
 */
void systable_inplace_update_begin(Relation fc_relation,
							  Oid fc_indexId,
							  bool fc_indexOK,
							  Snapshot fc_snapshot,
							  int fc_nkeys, const ScanKeyData *fc_key,
							  HeapTuple *fc_oldtupcopy,
							  void **fc_state)
{
	ScanKey		fc_mutable_key = palloc(sizeof(ScanKeyData) * fc_nkeys);
	int			fc_retries = 0;
	SysScanDesc fc_scan;
	HeapTuple	fc_oldtup;
	BufferHeapTupleTableSlot *fc_bslot;

	/*
	 * 目前，我们不允许并行更新。与常规更新不同，
	 * 这不应创建组合 CID，因此可能有可能放松
	 * 这个限制，但没有更多的思考和测试不行。
	 * 无论如何，目前还不清楚这是否有用。
	 */
	if (IsInParallelMode())
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
				 errmsg("cannot update tuples during a parallel operation")));

	/*
	 * 接受一个快照参数，为了对称性，但该函数根据需要推进
	 * 其快照，以到达更新的元组链的尾部。
	 */
	Assert(fc_snapshot == NULL);

	Assert(IsInplaceUpdateRelation(fc_relation) || !IsSystemRelation(fc_relation));

	/* 循环一个独占锁定的非更新元组的缓冲区。 */
	do
	{
		TupleTableSlot *fc_slot;

		CHECK_FOR_INTERRUPTS();

		/*
		 * 发出 heap_update（例如 GRANT）的进程以最大速度操作可能
		 * 会导致我们出现此错误。一个敌对的表所有者有更强的方法来
		 * 损害他们自己的表，因此这只是小问题。
		 */
		if (fc_retries++ > 10000)
			elog(ERROR, "giving up after too many tries to overwrite row");

		memcpy(fc_mutable_key, fc_key, sizeof(ScanKeyData) * fc_nkeys);
		fc_scan = systable_beginscan(fc_relation, fc_indexId, fc_indexOK, fc_snapshot,
								  fc_nkeys, fc_mutable_key);
		fc_oldtup = systable_getnext(fc_scan);
		if (!HeapTupleIsValid(fc_oldtup))
		{
			systable_endscan(fc_scan);
			*fc_oldtupcopy = NULL;
			return;
		}

		fc_slot = fc_scan->slot;
		Assert(TTS_IS_BUFFERTUPLE(fc_slot));
		fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	} while (!heap_inplace_lock(fc_scan->heap_rel,
								fc_bslot->base.tuple, fc_bslot->buffer,
								(void (*) (void *)) systable_endscan, fc_scan));

	*fc_oldtupcopy = heap_copytuple(fc_oldtup);
	*fc_state = fc_scan;
}

/*
 * systable_inplace_update_finish --- 就地更新的第二阶段
 *
 * 元组不能更改大小，因此它的头部字段和 null
 * 位图（如果有的话）也不会更改。
 */
void systable_inplace_update_finish(void *fc_state, HeapTuple fc_tuple)
{
	SysScanDesc fc_scan = (SysScanDesc) fc_state;
	Relation	fc_relation = fc_scan->heap_rel;
	TupleTableSlot *fc_slot = fc_scan->slot;
	BufferHeapTupleTableSlot *fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	HeapTuple	fc_oldtup = fc_bslot->base.tuple;
	Buffer		fc_buffer = fc_bslot->buffer;

	heap_inplace_update_and_unlock(fc_relation, fc_oldtup, fc_tuple, fc_buffer);
	systable_endscan(fc_scan);
}

/*
 * systable_inplace_update_cancel --- 放弃就地更新
 *
 * 这是进行无操作更新的一种替代方案。
 */
void systable_inplace_update_cancel(void *fc_state)
{
	SysScanDesc fc_scan = (SysScanDesc) fc_state;
	Relation	fc_relation = fc_scan->heap_rel;
	TupleTableSlot *fc_slot = fc_scan->slot;
	BufferHeapTupleTableSlot *fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	HeapTuple	fc_oldtup = fc_bslot->base.tuple;
	Buffer		fc_buffer = fc_bslot->buffer;

	heap_inplace_unlock(fc_relation, fc_oldtup, fc_buffer);
	systable_endscan(fc_scan);
}
